package de.hub.sar.itsec.sslfetcher.fetcher;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import de.hub.sar.itsec.sslfetcher.utils.Converter;

public class SSLFetcher implements IFetcher {
	private static final byte CONTENT_TYPE_HANDSHAKE = 0x16;
	private static final byte MAJOR_VERSION = 0x03;
	private static final byte MINOR_VERSION = 0x01;
	private static final byte HANDSHAKE_TYPE_CLIENT_HELLO = 0x01;
	private static final byte HANDSHAKE_TYPE_SERVER_HELLO = 0x02;
	private static final byte HANDSHAKE_TYPE_CERTIFICATE = 0x0b;
	private static final byte HANDSHAKE_TYPE_SERVER_HELLO_DONE = 0x16;
	private static final byte SESSION_ID_LENGTH = 0x00;
	private static final byte[] CIPHER_SUITES_LENGTH = new byte[]{0x00, 0x48};
	private static final byte[] CIPHER_SUITES = new byte[]{0x00, 
		(byte) 0xff, (byte) 0xc0, 0x0a, (byte) 0xc0, 0x14, 0x00, (byte) 0x88, 
		0x00, (byte) 0x87, 0x00, 0x39, 0x00, 0x38, (byte) 0xc0, 0x0f, 
		(byte) 0xc0, 0x05, 0x00, (byte) 0x84, 0x00, (byte) 0x35, (byte) 0xc0, 
		0x07, (byte) 0xc0, 0x09, (byte) 0xc0, 0x11, (byte) 0xc0, 0x13, 0x00, 
		0x45, 0x00, 0x44, 0x00, 0x33, 0x00, 0x32, (byte) 0xc0, 0x0c, 
		(byte) 0xc0, 0x0e, (byte) 0xc0, 0x02, (byte) 0xc0, 0x04, 0x00, 
		(byte) 0x96, 0x00, 0x41, 0x00, 0x05, 0x00, 0x04, 0x00, 0x2f, 
		(byte) 0xc0, 0x08, (byte) 0xc0, 0x12, 0x00, 0x16, 0x00, 0x13, 
		(byte) 0xc0, 0x0d, (byte) 0xc0, 0x03, (byte) 0xfe, (byte) 0xff, 0x00, 
		0x0a};
	private static final byte COMPRESSION_METHODS_LENGTH = 0x01;
	private static final byte[] COMPRESSION_METHODS = new byte[]{0x00};
	private static final byte[] EXTENSION_SERVER_NAME = new byte[]{0x00, 0x00};
	
	private final SecureRandom random;
	
	public SSLFetcher() {
		this.random = new SecureRandom();
	}

	@Override
	public List<byte[]> getCerts(final String host, final String domain, 
			final int port) throws FetchFailsException {
		Socket socket = null;
		OutputStream out;
		InputStream in;
		List<byte[]> certs;
		
		try {
			socket = new Socket(host, port);
			socket.setSoTimeout(5000);
			
			// get in/out stream
			out = socket.getOutputStream();
			in = socket.getInputStream();
			
			// send hello request
			this.writeClientHello(out, domain);
			out.flush();
			
			// wait until certs came in
			this.waitForCerts(in);
			
			// read certs
			certs = this.readCerts(in);
			
//			X509Certificate cert = X509Certificate.getInstance(certs.get(0));
//			System.out.println(cert);
//			System.out.println(cert.getSubjectAlternativeNames());
			closeSocket(socket);
		} catch (UnknownHostException e) {
			closeSocket(socket);
			throw new FetchFailsException(e);
		} catch (IOException e) {
			closeSocket(socket);
			throw new FetchFailsException(e);
		}
		return certs;
	}
	
	private static void closeSocket(Socket socket) throws FetchFailsException {
		if (socket != null) {
			try {
				socket.close();
			} catch (IOException e) {
				throw new FetchFailsException(e);
			}
		}
	}
	
	private List<byte[]> readCerts(final InputStream in) throws IOException {
		byte[] cert;
		int certsLength;
		int certLength;
		List<byte[]> result;
		
		result = new LinkedList<byte[]>();
		
		// get length
		certsLength = readLength(in, 3);
		
		while (certsLength > 0) {
			// save the certificates as byte array
			certLength = readLength(in, 3);
			cert = readBytes(in, certLength);
			result.add(cert);
			certsLength -= certLength + 3;
			// we do only want to load the main certificate
			// uncomment this line to load all certificates
			break;
		}
		
		return result;
	}
	
	private void waitForCerts(final InputStream in) throws IOException {
		byte inByte;
		int contentLength;
		int dataLength;
		
		while (true) {
			// read next content type
			inByte = (byte) in.read();
			if (inByte != CONTENT_TYPE_HANDSHAKE) {
				throw new IOException();
			}
			
			// ignore version
			in.read();
			in.read();
			
			for (contentLength = readLength(in, 2); contentLength > 0; 
					contentLength -= dataLength + 4) {
				inByte = (byte) in.read();
				
				// if hello ends or stream ends
				if (inByte == -1 || inByte == HANDSHAKE_TYPE_SERVER_HELLO_DONE) {
					throw new IOException();
				}
				
				dataLength = readLength(in, 3);
				
				// if certificates had been found
				if (inByte == HANDSHAKE_TYPE_CERTIFICATE) {
					return;
				}
				
				in.skip(dataLength);
			}
		}
	}
	
	private void writeClientHello(final OutputStream out, final String host) throws IOException {
		int length;
		
		length = host.getBytes().length;
	
//		out.write(CONTENT_TYPE_HANDSHAKE);
//		out.write(MAJOR_VERSION);
//		out.write(MINOR_VERSION);
//		out.write(Converter.getByteArray(2, 53 + length + CIPHER_SUITES.length + COMPRESSION_METHODS.length));
//		out.write(HANDSHAKE_TYPE_CLIENT_HELLO);
//		out.write(Converter.getByteArray(3, 49 + length + CIPHER_SUITES.length + COMPRESSION_METHODS.length));
//		out.write(MAJOR_VERSION);
//		out.write(MINOR_VERSION);
//		out.write(Converter.getByteArray(4, System.currentTimeMillis()));
//		out.write(this.getRandomBytes(28));
//		out.write(SESSION_ID_LENGTH);
//		out.write(CIPHER_SUITES_LENGTH);
//		out.write(CIPHER_SUITES);
//		out.write(COMPRESSION_METHODS_LENGTH);
//		out.write(COMPRESSION_METHODS);
//		out.write(Converter.getByteArray(2, length + 9));
//		out.write(EXTENSION_SERVER_NAME);
//		out.write(Converter.getByteArray(2, length + 5));
//		out.write(Converter.getByteArray(2, length + 3));
//		out.write(Converter.getByteArray(3, length));
//		out.write(host.getBytes());

	
		// so wird das ganze als ein Paket gesendet, was das ganze einfacher zum Debuggen macht
		ByteArrayOutputStream tmp = new ByteArrayOutputStream( );

		
		length = host.getBytes().length;
		
		tmp.write(CONTENT_TYPE_HANDSHAKE);
		tmp.write(MAJOR_VERSION);
		tmp.write(MINOR_VERSION);
		tmp.write(Converter.getByteArray(2, 53 + length + CIPHER_SUITES.length + COMPRESSION_METHODS.length));
		tmp.write(HANDSHAKE_TYPE_CLIENT_HELLO);
		tmp.write(Converter.getByteArray(3, 49 + length + CIPHER_SUITES.length + COMPRESSION_METHODS.length));
		tmp.write(MAJOR_VERSION);
		tmp.write(MINOR_VERSION);
		tmp.write(Converter.getByteArray(4, System.currentTimeMillis()));
		tmp.write(this.getRandomBytes(28));
		tmp.write(SESSION_ID_LENGTH);
		tmp.write(CIPHER_SUITES_LENGTH);
		tmp.write(CIPHER_SUITES);
		tmp.write(COMPRESSION_METHODS_LENGTH);
		tmp.write(COMPRESSION_METHODS);
		tmp.write(Converter.getByteArray(2, length + 9));
		tmp.write(EXTENSION_SERVER_NAME);
		tmp.write(Converter.getByteArray(2, length + 5));
		tmp.write(Converter.getByteArray(2, length + 3));
		tmp.write(Converter.getByteArray(3, length));
		tmp.write(host.getBytes());

		tmp.writeTo(out);
	}
	
	private byte[] getRandomBytes(final int size) {
		byte[] result;
		
		result = new byte[size];
		this.random.nextBytes(result);
		
		return result;
	}
	
	private static byte[] readBytes(final InputStream in, final int nbrOfBytes) throws IOException {
		byte[] result;
		int size;
		
		result = new byte[nbrOfBytes];
		
		for (int off = 0; off < nbrOfBytes; off += size) {
			size = in.read(result, off, nbrOfBytes - off);
			if (size == -1) {
				throw new IOException();
			}
		}
		
		return result;
	}
	
	private static int readLength(final InputStream in, final int nbrOfBytes) throws IOException {
		return Converter.getInt(readBytes(in, nbrOfBytes));
	}
}